from __future__ import division
import netCDF4
import copy
import numpy
import datetime
import re

def first_element(input):
    '''Find the first element of the input. If the input is a scalar,
    then the first element is that scalar.'''
    try:
        input.__iter__
    except:
        # Return a non-iterable scalar
        return input
    else:
        if isinstance(input, numpy.ndarray):
            # Return the 1st element of a numpy.ndarray
            return input[(0,) * input.ndim]
        else:
            try:
                # Return the 1st element of a list or tuple
                return input[0]
            except IndexError:
                # Return an empty list or tuple
                return input
#---End: def

def last_element(input):
    '''Find the last element of the input. If the input is a scalar,
    then the first element is that scalar.'''
    try:
        input.__iter__
    except:
        # Return a non-iterable scalar
        return input
    else:
        if isinstance(input, numpy.ndarray):
            # Return the 1st element of a numpy.ndarray
            return input[(-1,) * input.ndim]
        else:
            try:
                # Return the 1st element of a list or tuple
                return input[-1]
            except IndexError:
                # Return an empty list or tuple
                return input
#---End: def

# Fix these regexs
netCDF4.netcdftime.ISO8601_REGEX = re.compile(r"(?P<year>[0-9]{1,4})(-(?P<month>[0-9]{1,2})(-(?P<day>[0-9]{1,2})"
                                              r"(((?P<separator1>.)(?P<hour>[0-9]{1,2}):(?P<minute>[0-9]{1,2})(:(?P<second>[0-9]{1,2})(\.(?P<fraction>[0-9]+))?)?)?"
                                              r"((?P<separator2>.?)(?P<timezone>Z|(([-+])([0-9]{1,2}):([0-9]{1,2}))))?)?)?)?"
                                              )

netCDF4.netcdftime.TIMEZONE_REGEX = re.compile("(?P<prefix>[+-])(?P<hours>[0-9]{1,2}):(?P<minutes>[0-9]{1,2})")


# Set month lengths for leap years (_month_lengths[1,1:]) and
# non-leap years (_month_lengths[0,1:])
_month_lengths = numpy.array([[-99, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
                              [-99, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]],
                             dtype='int32')

# Set conversion factor between different units with the same origin
# and calendar
_cv_units2days = {
    'days'   : 1,
    'hours'  : 1/24.0,
    'minutes': 1/1440.0,
    'seconds': 1/86400.0,
    }

# Define nanosecond accuracy for various units
_units_atol = {
    'days'   : 1/8.64e13,
    'hours'  : 1/3.6e12,
    'minutes': 1/6e10,
    'seconds': 1/1e9,
    }
 
_equivalent_units = {
    's'      : ('s', 'sec', 'secs', 'second', 'seconds'),
    'sec'    : ('s', 'sec', 'secs', 'second', 'seconds'),
    'secs'   : ('s', 'sec', 'secs', 'second', 'seconds'),
    'second' : ('s', 'sec', 'secs', 'second', 'seconds'),
    'seconds': ('s', 'sec', 'secs', 'second', 'seconds'),
    'min'    : ('min', 'mins', 'minute', 'minutes'),
    'mins'   : ('min', 'mins', 'minute', 'minutes'),
    'minute' : ('min', 'mins', 'minute', 'minutes'),    
    'minutes': ('min', 'mins', 'minute', 'minutes'),
    'h'      : ('h', 'hr', 'hrs', 'hour', 'hours'),
    'hr'     : ('h', 'hr', 'hrs', 'hour', 'hours'),
    'hrs'    : ('h', 'hr', 'hrs', 'hour', 'hours'),
    'hour'   : ('h', 'hr', 'hrs', 'hour', 'hours'),
    'hours'  : ('h', 'hr', 'hrs', 'hour', 'hours'),
    'd'      : ('d', 'day', 'days'),
    'day'    : ('d', 'day', 'days'),    
    'days'   : ('d', 'day', 'days'),    
    }

_equivalent_calendar = {
    'standard'           : ('standard', 'gregorian'),
    'gregorian'          : ('standard', 'gregorian'),
    'proleptic_gregorian': ('proleptic_gregorian',),
    'noleap'             : ('noleap',),         
    'julian'             : ('julian',),         
    'all_leap'           : ('all_leap',),         
    '365_day'            : ('365_day',),         
    '366_day'            : ('366_day',),          
    '360_day'            : ('360_day',),          
    }

def month_length(year, month, calendar):
    '''

Find month lengths in days for each year/month pairing in the input
numpy arrays 'year' and 'month', which must have the same
shape. 'calendar' must be one of the standard CF calendar types.
'''
    if calendar in ('standard', 'gregorian'):
        leap = numpy.where(year % 4 == 0, 1, 0)
        leap = numpy.where((year > 1582) & 
                           (year % 100 == 0) & (year % 400 != 0), 
                           0, leap)
    elif calendar == '360_day':
        days_in_month = numpy.empty(month.shape, dtype='int32')
        days_in_month.fill(30)
        return days_in_month

    elif calendar in ('all_leap', '366_day'):
        leap = numpy.zeros(month.shape, dtype='int32')

    elif calendar in ('no_leap', '365_day'):
        leap = numpy.ones(month.shape, dtype='int32')

    elif calendar == 'proleptic_gregorian':
        leap = numpy.where(year % 4 == 0, 1, 0)
        leap = numpy.where((year % 100 == 0) & (year % 400 != 0), 
                           0, leap)

    days_in_month = [_month_lengths[l, m] for l,m in zip(leap.flat, 
                                                         month.flat)]
    days_in_month = numpy.array(days_in_month, dtype='int32').reshape(month.shape)

    return days_in_month
#--- End: def        

def _proper_date(year, month, day, calendar, fix=False):
    '''

Given equally shaped numpy arrays of 'year', 'month', 'day' adjust
them *in place* to be proper dates. 'calendar' must be one of the
standard CF calendar types.

For example: 2000/26/1 -> 2002/2/1

If 'day' is illegal in the new date then a ValueError is raised,
unless 'fix' is True, in which case 'day' is lowered to the nearest
legal date.

For example: 2000/2/31 -> ValueError if 'fix' is False
             2000/2/31 -> 2002/2/28  if 'fix' is True
             2000/2/99 -> 2002/2/28  if 'fix' is True
    '''
#    y, month = divmod(month, 12)
#    year += y    

    year      += month // 12
    month[...] = month % 12

    mask       = (month == 0)
    year[...]  = numpy.where(mask, year-1, year)
    month[...] = numpy.where(mask, 12, month)
    del mask

    days_in_month = month_length(year, month, calendar)

    if fix:
        day[...] = numpy.where(day > days_in_month, days_in_month, day)
    elif (day > days_in_month).any():
        raise ValueError, "Illegal date(s) in %s calendar" % calendar

    return year, month, day
#--- End: def



class DateTime(netCDF4.netcdftime.datetime):
    '''
    
More or less (well, ok, less rather than more at the moment) like a
datetime.datetime object that allows non-standard calendars
(cf. netCDF4.netcdftime.utime.datetime)

'''
    def __eq__(self, other):        
        for attr in ('year', 'month', 'day', 'hour', 'minute', 'second'):
            if getattr(self, attr) != getattr(other, attr):
                return False

        return True
    #--- End: def

    def __ne__(self, other):
        return not self.__eq__(other)
    #--- End: def

    def replace(self, year=None, month=None, day=None, 
                hour=None, minute=None, second=None):
        
        new = copy.copy(self)

        [setattr(new, attr, value) \
             for attr, value in zip(('year', 'month', 'day', 'hour', 'minute', 'second'),
                                    ( year ,  month ,  day ,  hour ,  minute ,  second )) \
             if value is not None]

        new.dayofwk = -1
        new.dayofyr = 1

        return new
    #--- End: def

#--- End: class

class Time(object):

    '''

A Time object contains an arbitrarily dimensioned array of times

the input may be a sequence or a scalar. In particular, a Time
instance may be multidimensional by passing in a multidimensional
numpy array. In any case, the times within the Time object may be
indexed with numpy-style indexing.

The times are always available as numeric times or as datetime-like
objects. A datetime-like object is either a python datetime.datetime
object, or a DateTime object, which behaves liek the former but
supperts non-standard calendars.

A Time instance always has the following attributes:

units       : The numeric times' units, e.g. 'days'

units_string: The numeric times' complete CF units string, 
              e.g. 'days since 2000-1-1'

calendar    : The times' calendar, e.g. 'gregorian'

years, ..., microseconds: those components of the times

The objects's numeric times are avaialable as a numpy array from the attributes
'array' and 'varray'

The object's datetimes are avaialable as a numpy array of
datetime-like objects from the attributes 'datetime' and 'vdatetime'.

An instance always stores times as numeric times. Datetime-like times
are created on demand (or with the '_create_datetime' method). Once
datetime-like times have been created they are also stored in the
object for fast future access.

Numeric to datetime (and vice versa) transformation are currently done
with netCDF4.netcdftime.utime methods. This will change, as they are
not quite general enough. The replacement would be pretty similar, but
with a few modifications, I suspect.

Operator overloading
--------------------
The following are all set: 

+, -,  -=, +=, ==, !=, <, <=, >, >=

    '''
    __slots__ = ['_data', 
                 '_datetime', 
                 '_utime',
                 'format']

    def __init__(self, input, units=None, calendar='standard',
                 _utime=None):

        '''

Initialize a Time instance with 'input' times. 'input' may be a scalar
(numermic time or datetime-like object) or a sequence of such
scalars. If a sequence is provided, numeric times may not be mixed
with datetime-like objects.

A netCDF4.netcdftim.utime is derived from 'units' (a complete CF units
string) and 'calendar' and stored in self._utime.

If '_utime' is provided then then self._utime is a copy of it and
'units' and 'calendar' are ignored. This is to speed up creation of
new instances when the units and calendar are already known.

If units of months, years, julian_years, common_years or leap_years
are provided then they are changed to days and stored numeric times
are adjusted if required.
        
        '''

        # Put input into a numpy array
        times = numpy.array(input)
        
        if times.dtype.kind in 'fi':
            # Times are numeric times => put them straight into the
            # _data attribute
            self._data = times
        else:
            first = first_element(times)
            if isinstance(first, (datetime.datetime, DateTime)):
                # Times are datetime-like objects => put them straight
                # into the _datetime attribute
                self._datetime = times
                if units is None:
                    # Set default units
                    units = 'days since %s-1-1' % first.year
            else:            
                raise ValueError("Can't intialize '%s' with '%s'" %
                                 (self.__class__.__name__,
                                  first.__class__.__name__))
            
        if _utime is not None:
            # Use the given utime object instead of creating one from
            # 'units' and 'calendar'
            self._utime = copy.deepcopy(_utime)

        else:
            # Create a utime object from 'units' and 'calendar'
            if units is None:
                raise TypeError("Must set units or utime when initializing a '"+
                                self.__class__.__name__+
                                "' instance with numeric time")

            units_split = units.split()
            units0 = units_split[0].lower()
            
            # Convert undesirable units to days
            if units0 == 'months':
                units = ' '.join(('days', units_split[1:]))
                scale_data = 30.436849898416668 # = 365.242198781/12
            elif units0 == 'years':
                units = ' '.join(('days', units_split[1:]))
                scale_data = 365.242198781 
            elif units0 == 'julian_years':
                units = ' '.join(('days', units_split[1:]))
                scale_data = 365.2425      
            elif units0 == 'common_years':
                units = ' '.join(('days', units_split[1:]))
                scale_data = 365.0
            elif units0 == 'leap_years':
                units = ' '.join(('days', units_split[1:]))
                scale_data = 366.0

            self._utime = netCDF4.netcdftime.utime(units, calendar=calendar)
        #--- End: if

        # Create the _data attribute if it doesn't exist
        try:
            self._data
        except AttributeError:
            self._data = self._utime.date2num(self._datetime)
        else:
            # Scale the data if we have changed the units
            try:
                self._data /= scale_data
            except UnboundLocalError:
                pass

        # Format for printing
        self.format = '%Y-%m-%dT%H:%M:%S'
    #--- End: def

    def _zero_origin_days(self):
        '''

Return a numpy array of numeric times in units of 'days since ...'
'''
        return self._data * _cv_units2days[self.units] + self._jd0

    def _create_datetime(self):
        '''

Create a numpy array of datetime-like objects from numeric times,
unless it already exists in `self._datetime`
'''
        try:
            self._datetime
        except AttributeError:
            self._datetime = numpy.array(self._utime.num2date(self._data))
    #--- End: def    

    def _datetime_getattr(self, attr):
        '''

Return a numpy array of attributes of the datetime-like objects stored
in `self._datetime`. This is used in `self.years`, for example.
'''
        attrs = [getattr(dt, attr) for dt in self.vdatetime.flat]
        return numpy.reshape(numpy.array(attrs), self.shape)
    #--- End: def      

    def __repr__(self):
        '''
x.__repr__() <==> repr(x)
'''
        self._create_datetime()


        o_brackets = '[' * ndim
        c_brackets = ']' * ndim
        first = self.vdatetime.first_datum
        last  = self.vdatetime.last_datum

        size = self.vdatetime.size

        if size == 1:
            return "<CF %s: %s%s%s" % (self.__class__.__name__,
                                       o_brackets, first, c_brackets)
        elif size >= 3:
            return "<CF %s: %s%s, ..., %s%s" % (self.__class__.__name__,
                                                o_brackets, first, last,
                                                c_brackets)
        else:
            return "<CF %s: %s%s, %s%s" % (self.__class__.__name__,
                                           o_brackets, first, last, c_brackets)
    #--- End: def

    def __str__(self):
        '''
x.__str__() <==> str(x)

'''
        self._create_datetime()
        return str(self.vdatetime)
    #--- End: def

    def __len__(self):
        '''
x.__len__() <==> len(x), the number of times in the object
'''
        return len(self._data)
    #--- End: def

    def sort(axis=None, kind='quicksort', order=None):
        '''

Return a new Time object with its times sorted into ascending
numerical order. Refer to `numpy.sort` for details.
'''
        sorted_data = numpy.sort(self._data, axis=axis, kind=kind, order=order)
        return type(self)(sorted_data, _utime=self._utime)
    #--- End: def
    
    def equivalent_calendar(self, other):
        '''

Return True if the given object has an equivalent calendar, False
otherwise.

**Examples**

>>> t.calendar, u.calendar
('standard', 'standard')
>>> t.equivalent_calendar(u)
True
>>> t.equivalent_calendar('standard')
True

>>> t.calendar, u.calendar
('standard', 'greogorian')
>>> t.equivalent_calendar(u)
True

>>> t.calendar, u.calendar
('standard', 'no_leap')
>>> t.equivalent_calendar(u)
False
'''

        try:
            calendar = other.calendar
        except AttributeError:
            calendar = str(other)

        if calendar not in _equivalent_calendar[self.calendar]:
            return False
        
        return True
    #--- End: def 

    def change_units(self, units):
        '''

Create a `Time` object with the same dates but different units.

:argument units: The units to change to. May take one of three forms:
                 
                 1. A string giving the new unit, such as
                    `seconds`. The reference date is assumed to remain
                    unchanged.

                 2. A CF compliant 'units' attribute string giving the
                    new unit and a reference date, such as `seconds
                    since 2000-1-1`.

                 3. A Time object, from which the new unit and a
                    reference date are taken. An exception will be
                    raised if this Time object has a different
                    calendar.

:type units: `str` or `Time`

:returns: A Time object

*Examples*

>>> t = cf.Time([1,2,3], units='days since 1961-12-01')
>>> u = t.change_units('seconds since 2000-1-1')
>>> print t
[1961-12-02 00:00:00 1961-12-03 00:00:00 1961-12-04 00:00:00]
>>> print u
[1961-12-02 00:00:00 1961-12-03 00:00:00 1961-12-04 00:00:00]
>>> t.varray
array([1, 2, 3])
>>> u.varray
array([ -1.20173760e+09,  -1.20165120e+09,  -1.20156480e+09])

Note that the choice of units is arbitrary, so two Time objects with
the same calendar which represent the same dates are always considered
equal.

>>> t == u
array([ True,  True,  True], dtype=bool)
>>> t.equals(u)
True

'''
        if isinstance(units, Time):
            # 'units' is a Time object
            if not self.equivalent_calendar(units):
                raise ValueError, "Can't change units for different calendars: '%s', '%s'" % \
                    (self.calendar, units.calendar)
            _utime = copy.deepcopy(units._utime)
        else:
            try:                
                # 'units' is a unit with a reference date, such as
                # 'days since 2000-1-1'
                _utime = netCDF4.netcdftime.utime(units, calendar=self.calendar)
            except (IndexError, ValueError):
                if units in netCDF4.netcdftime._units:
                    # 'units' is a unit with no reference date, such
                    # as 'days', so use self.origin for the reference
                    # date
                    unit_string = '%s since %s' % (units, self.origin)
                    _utime = netCDF4.netcdftime.utime(unit_string, calendar=self.calendar)
                else:
                    raise TypeError, "Can't get units from '%s'" % units
                
        if self.equivalent_units(_utime):
            return copy.deepcopy(self)
        
        # Change the numeric time array for the new units
        new = copy.deepcopy(self)    
        new._data  = new._zero_origin_days() # Must do this before resetting _utime
        new._utime = _utime                  
        new._data  = (new._data - new._jd0) / _cv_units2days[new.units]

        return new
    #--- End: def

    def __getitem__(self, index):
        return type(self)(self._data[index], _utime=self._utime)

    def __setitem__(self, index, value):
        if isinstance(value, Time):
            # 'value' is a Time object
            try:
                value1 = value.change_units(self)
            except ValueError as error:
                raise ValueError, 'Can\'t set item: %s' % error
            self._data[index] = value1._data
        else:
            value = numpy.array(value) # dch check for feedback
            first = first_element(value)
            if isinstance(first, (DateTime, datetime.datetime)):
                # 'value' is a datetime-like object, or an array of them
                self._data[index] = self._utime.date2num(value)
            else:
                raise TypeError, "Can't set item from '%s'" % \
                    first.__class__.__name__

        try:        
            del self._datetime
        except AttributeError:
            pass        
    #--- End: def    

    @property
    def shape(self):
        '''
Tuple of time array dimensions
'''
        return self._data.shape
    #--- End: def    

    @property
    def size(self):
        '''
Number of times in the object
        '''
        return self._data.size
    #--- End: def    

    @property
    def ndim(self):
        '''
Number of dimensions in the object
'''
        return self._data.ndim
    #--- End: def    

    @property
    def dtype(self):
        '''
Data type of numeric times
'''
        return self._data.dtype
    #--- End: def    

    @property
    def datetime(self):
        '''

Return a numpy array copy of datetime-like objects. Creates
`self._datetime` if it didn't exist.
'''
        self._create_datetime()
        return self._datetime.copy()
    #--- End: def

    @property
    def vdatetime(self):
        '''

Return a numpy array view of datetime-like objects. Creates
`self._datetime` if it didn't exist.
'''
        self._create_datetime()
        return self._datetime.view()
    #--- End: def

    @property
    def array(self):
        '''

Return a numpy array copy of the numeric times.
'''
        return self._data.copy()
    #--- End: def 

    @property
    def varray(self):
        '''

Return a numpy array view of the numeric times.
'''
        return self._data.view()

    @property
    def calendar(self):
        '''
Return the object's calendar (e.g'. 'all_leap')
'''
        return self._utime.calendar
    
    @property
    def unit_string(self):
         '''
Return the object's units string (e.g. 'days since 3-4-5')
'''
         return self._utime.unit_string
    
    @property
    def units(self):
        '''
Return the object's units string (e.g. 'days')
'''
        return self._utime.units
    
    @property
    def _jd0(self):
        '''
Return the object's numeric times' julian day origin
'''
        return self._utime._jd0
    
    @property
    def tzoffset(self):
        '''
Return the object's numeric times' time zone offset
'''
        return self._utime.tzoffset    
    
    @property
    def origin(self):
        '''
Return the object's numeric times' origin as a datetime-like object
'''
        return self._utime.origin
    
    @property
    def year(self):
        '''
Return a numpy array of each time's year
'''
        return self._datetime_getattr('year')
    
    @property
    def month(self):
        '''
Return a numpy array of each time's month
'''
        return self._datetime_getattr('month')
    
    @property
    def day(self):
        '''
Return a numpy array of each time's day
'''
        return self._datetime_getattr('day')
    
    @property
    def hour(self):
        '''
Return a numpy array of each time's hour
'''
        return self._datetime_getattr('hour')
    
    @property
    def minute(self):
        '''
Return a numpy array of each time's minute
'''
        return self._datetime_getattr('minute')
    
    @property
    def second(self):
        '''
Return a numpy array of each time's second
'''
        return self._datetime_getattr('second')
    
    @property
    def microsecond(self):
        '''
Return a numpy array of each time's microsecond
'''
        return self._datetime_getattr('microsecond')
    
    def equals(self, other):        
        '''
        
Return True if two Time objects are equivalent, False otherwise
'''

        if not isinstance(other, Time):
            return False

        if not self.equivalent_calendar(other):
            return False

        if (self != other).any():
            return False

        return True
    #--- End: def

    def __eq__(self, other):
        '''

Return a boolean numpy array of where two Time instances' numeric
times are equal to within the minimum resolvable time resolution
(currently 1e-9 seconds).
'''
        if not isinstance(other, Time):
            return False

        try:
            other1 = other.change_units(self)
        except ValueError as error:
            raise ValueError, 'Can\'t compare Time objects: %s' % error

        return numpy.absolute(self._data - other1._data) <= _units_atol[self.units]
    #--- End: def

    def __ne__(self, other):
        return ~(self == other)

    def __lt__(self, other):
        '''

Return a boolean numpy array of where one Time instance's numeric
times are strictly less than anothers.
'''

        if not isinstance(other, Time): 
            return False

        try:
            other1 = other.change_units(self)
        except ValueError as error:
            raise ValueError, 'Can\'t compare Time objects: %s' % error

        return self._data < other1._data
    #--- End: def


    def __le__(self, other):

        if not isinstance(other, Time):
            return False

        try:
            other1 = other.change_units(self)
        except ValueError as error:
            raise ValueError, 'Can\'t compare Time objects: %s' % error

        return self._data <= other1._data
    #--- End: def

    def __gt__(self, other):
        return ~(self <= other)

    def __ge__(self, other):
        return ~(self < other)

    def __add__(self, other):
        '''

Add a time interval given by a TimeInterval (or datetime.timedelta)
object to all of the times in this Time instance. Return a new object.
'''          
        data = self._data.copy()

        try:                
            # Try for 'other' being a TimeInterval-like object.
            seconds = other._total_seconds
            months  = other.months
            years   = other.years
        except AttributeError:
            # Try for 'other' being a datetime.timedelta-like
            # object.
            try:
                seconds = (other.days*86400 + other.seconds + 
                           other.microseconds/1e6)
            except AttributeError:
                raise TypeError("Bad operand type(s) for +: '"+
                                self.__class__.__name__+"' and '"+
                                other.__class__.__name__+"'")
        else:
            # If 'other' is a TimeInterval which specifies nonzero
            # years or months then add those on first
            if (months != 0).any() or (years != 0).any():
                year, month, day = _proper_date(self.year  + years,
                                                self.month + months,
                                                self.day,
                                                self.calendar, fix=True)
                datetime = [DateTime(y,m,d) for y,m,d in zip(year.flat,
                                                             month.flat, 
                                                             day.flat)]
                data = self._utime.date2num(datetime).reshape(self.shape)
        #--- End: try
                    
        if self.units == 'days':
            data += seconds/86400.0
        elif self.units == 'seconds':
            data += seconds
        elif self.units == 'minutes':
            data += seconds/60.0
        elif self.units == 'hours':
            data += seconds/3600.0  
        else:
            raise ValueError,'units????'
                    
        return type(self)(data, _utime=self._utime)
    #--- End: def 

        return other + self
    #--- End: def 

    def change_calendar(self, calendar, fix=False):
        '''

Give a new calendar to Time object, possibly adjusting dates which are
illegal in the new calendar.

:argument calendar: The calendar to change to. May take one of two forms:
 
                    1. A CF compliant 'calendar' attribute giving the
                       new calendar, such as 'gregorian'.
 
                    2. A Time object, from which the new calendar is
                       taken.
 
:type calendar: `str` or `Time`

:parameter fix: If False then raise an exception when encountering
                dates which are illegal in the new calendar, such as
                2001-02-30 in the gregorian calendar.

                If True then replace dates which are illegal in the
                new calendar with the closest earlier legal date. For
                example 2001-02-30 in gregorian calendar would be
                replaced with 2000-02-28.

:type fix: `bool`

:returns: A Time instance

*Examples*

>>> t = cf.Time([1,2,3], units='days since 2000-02-27', calendar='gregorian')
>>> u = t.change_calendar('360_day')
>>> print t
[2000-02-28 00:00:00 2000-02-29 00:00:00 2000-03-01 00:00:00]
>>> print u
[2000-02-28 00:00:00 2000-02-29 00:00:00 2000-03-01 00:00:00]

>>> t = cf.Time([1,2,3], units='days since 2000-02-27', calendar='360_day')
>>> u = t.change_calendar('gregorian', fix=True)
>>> print(t)
[2000-02-28 00:00:00 2000-02-29 00:00:00 2000-02-30 00:00:00]
>>> print u
[2000-02-28 00:00:00 2000-02-29 00:00:00 2000-02-29 00:00:00]

>>> u = t.change_calendar('gregorian')
ValueError: Can't convert calendar: Illegal date(s) in gregorian calendar

        '''        
        if self.equivalent_calendar(calendar):
            return copy.deepcopy(self)
        
        if calendar in netCDF4.netcdftime._calendars:
            calendar = calendar
        else:
            try:
                calendar = calendar.calendar
            except AttributeError:
                raise TypeError, "Unknown calendar type"

        new = copy.deepcopy(self)
        
        self._create_datetime()        

        new._utime.calendar = calendar
        try:
            year, month, day = _proper_date(self.year, self.month, self.day,
                                            new.calendar, fix=fix)
        except ValueError as error:
            raise ValueError, "Can't convert calendar: %s" % error

        new._datetime = numpy.array([DateTime(y,m,d) for y,m,d in zip(year.flat,
                                                                      month.flat, 
                                                                      day.flat)])
        new._datetime = new._datetime.reshape(new.shape)
        new._data     = new._utime.date2num(new._datetime)
        return new
    #--- End: def       

    def equivalent_units(self, other):
        '''

Return True if the other Time instance has eqivalent units'''

        if (self._jd0     != other._jd0     or
            self.tzoffset != other.tzoffset):
            return False

        if other.units not in _equivalent_units[self.units]:
            return False

        return True
    #--- End: def
    
    def __sub__(self, other):
        '''

Subtract a time interval given by a TimeInterval (or
datetime.timedelta) object from all of the times in this Time
instance. Return a new object.

Or subtract a two Time objects, return a TimeInterval object
'''          
        try:
            return self + -other
        except TypeError:
            try:
                if self.calendar != other.calendar:
                    raise ValueError, 'asdasdasd 1278hsh dch'            
            except AttributeError:   
                raise TypeError("Bad operand type(s) for -: '"+
                                self.__class__.__name__+"' and '"+
                                other.__class__.__name__+"'")
            else:
                x = self._zero_origin_days()
                y = other._zero_origin_days()
                return TimeInterval(days = x-y)
    #--- End: def 

    def __iadd__(self, other): 
        '''
x.__iadd__(y) <==> x+=y
        '''
        return self + other

    def __isub__(self, other):
        '''
x.__isub__(y) <==> x-=y
        '''
        return self - other

    def mid_point(self, other):
        '''

Return a Time instance for the mid point betwen two times

:returns: At `Time` instance

*Examples*

>>> t0 = Time([0  , 1  ], 'days since 2000-1-1')
>>> t1 = Time([2.5, 3.5], 'days since 2000-6-19')
>>> print t0.mid_point(t1)
[2000-03-027 00:00:00 2000-03-28 06:00:00]

'''
        try:
            other1 = other.change_units(self)
        except ValueError as error:
            raise ValueError, 'Can\'t find Time mid point: %s' % error

        return type(self)(0.5*(self._data + other1._data), _utime=self._utime)
    #--- End: def

#--- End: class

def _createTimeInterval(Y, M, D, h, m, s, ms, _ts):
    return TimeInterval(Y, M, D, h, m, s, ms, _total_seconds=_ts)


class TimeInterval(object):
    '''

A TimeInterval object represents a sequence of durations, the
differences between two times. It's very much like a
datetime.timedelta object but allows intervals to be defined in terms
of years, months, days, hours, minutes, seconds, and microseconds, or
any comnbination of these.

Inputs may be sequences or scalars. When the two types are mixed, the
scalars are may be broadcast to be of the same size and shape as teh
sequences. In particular, a TimeInterval instance may be
multidimensional by passing in multidimensional numpy arrays. In any
case, the time intervals within the TimeInterval object may be indexed
with numpy-style indexing.

Resolution
----------
The smallest allowed time interval is 1 nanosecond.

Days and shorter time periods
-----------------------------
1. These be input as integers or flaots, or arrays of integer or float
   type.

2. Inside the object they modified values are stored as follows:
   days        : any integer
   hours       : 0 <= integer <= 23
   minutes     : 0 <= integer <= 59
   seconds     : 0 <= integer <= 59
   microseconds: 0 <= integer or float <= 999999.999 

Years and months
----------------
These are treated distinctly to the shorter time periods (days to
microseconds):

1. Years and months must be input as integers, or arrays of integer
   type.

2. Whilst years may be modified by months (1 year 13 months -> 2 years
   1 month), months are not modified by days (1 month 99 days -> 1
   month 99 days0

3. When a time interval combines years or months with shorter time
   periods, any operation acting on the time interval are done on the
   years and months seperately to the shorter time periods. For
   example:

   (1 year 7 months 20 days) * 2 -> 3 years 2 months 40 days

   Similarly, when a TimeInterval is added to a Time object, non-zero
   years and months are added first and days and then shorter time
   periods are added independently.

Operator overloading
--------------------
The following are all set: 

+, -, *, /, -=, +=, *=, /=, +, -, ==, !=, <

(I haven't coded up the other comaprison operators yet)

    '''

    # The attributes described within __slots__ are the only
    # attributes this class will ever need.
    __slots__ = ['_years',
                 '_months',      
                 '_days',        
                 '_hours',       
                 '_minutes',     
                 '_seconds',  
                 '_microseconds',
                 '_total_seconds']

    def __init__(self, years=0, months=0, 
                 days=0, hours=0, minutes=0, seconds=0, 
                 microseconds=0, interval=None, _total_seconds=None):

        try:
            (years, month, days, hours, minutes,
             seconds, microseconds) = interval
        except TypeError:
            pass

        # Set private attributes from input arguments
        self._years        = numpy.array(years)
        self._months       = numpy.array(months)
        self._days         = numpy.array(days)
        self._hours        = numpy.array(hours)
        self._minutes      = numpy.array(minutes)
        self._seconds      = numpy.array(seconds)
        self._microseconds = numpy.array(microseconds)

        # Check that certain arguments are integer valued            
        for input in (self._years, self._months): #, self._microseconds):
            if input.dtype.kind != 'i':
                raise TypeError, "Can't initialize '%s' with non-integer years or months" % \
                    self.__class__.__name__
            
        # adjust the attributes so that they lie in their appropriate
        # ranges. This also sets the attribute '_total_days'
        if _total_seconds is None:
            self._adjust_attributes()
        else:
            self._total_seconds = _total_seconds
    #--- End: def

    def _adjust_attributes(self):
        ''' 
Do: 

days        : any integer
hours       : 0 <= integer <= 23
minutes     : 0 <= integer <= 59
seconds     : 0 <= integer <= 59
microseconds: 0 <= integer or float <= 999999.999 

and calculate _total_seconds
'''
        _total_seconds = numpy.around((self._days*86400 + self._hours*3600 + self._minutes*60 + 
                                       self._seconds + self._microseconds*1e-6), 9)
        
        seconds, microseconds = divmod(_total_seconds, 1)
        minutes, seconds      = divmod(numpy.int64(seconds), 60)
        hours  , minutes      = divmod(minutes, 60)
        days   , hours        = divmod(hours, 24)

        # Nanosecond accuracy`
        microseconds = numpy.around(microseconds*1000000, 3)
 
        # Reset private attributes
        self._days          = days         
        self._hours         = hours        
        self._minutes       = minutes      
        self._seconds       = seconds      
        self._microseconds  = microseconds 
        self._total_seconds = _total_seconds
    #--- End: def

    def __getitem__(self, index):
        '''

Index a TimeInterval with numpy indexing, return a new TimeInterval
object.
'''
        interval = []

        for attr in ('_years', '_months', '_days', '_hours', '_minutes', 
                     '_seconds', '_microseconds'):
            array = getattr(self, attr)
            try:
                interval.append(array[index])
            except IndexError:
                interval.append(array[...])

        return type(self)(interval=interval,
                          _total_seconds=self._total_seconds[index])
    #--- End: def

    def __setitem__(self, index):
        '''

'''
        raise NotImplementedError, "yet to write __setitem__ ...."
    #--- End: def

    @property
    def array(self):
        '''

Return a numpy array of 1d, size 1 TimeInterval objects, made up from
each time interval in the object
'''
        return numpy.array(map(_createTimeInterval,
                               self.years.flat, 
                               self.months.flat,
                               self.days.flat, 
                               self.hours.flat,
                               self.minutes.flat, 
                               self.seconds.flat,
                               self.microseconds.flat,
                               self._total_seconds.flat)).reshape(self.shape)
    #--- End: def 
        
    @property
    def size(self):
        return max(self._years.size,
                   self._months.size,
                   self._total_seconds.size)
    #--- End: def

    @property
    def ndim(self):
        return max(self._years.ndim,
                   self._months.ndim,
                   self._total_seconds.ndim)
    #--- End: def

    @property
    def shape(self):
        ndim = self.ndim
        if ndim == 0:
            return ()
        if ndim == 1:
            return (self.size,)       

        ndim_array = numpy.array([self._years.ndim,
                                  self._months.ndim,
                                  self._total_seconds.ndim])
        array = (self._years,
                 self._months,
                 self._total_seconds)
        return array[ndim_array.argmax()].shape
    #--- End: def

    def  __str__(self):
        '''
x.__str__() <==> str(x)

Use ISO 8601 standard formatting (http://en.wikipedia.org/wiki/ISO_8601#Durations)
'''
        if self.size == 1:
    
            seconds      = self.seconds
            microseconds = self.microseconds
            if microseconds > 0:
                seconds = ('%.9f' % (seconds + microseconds*1e-6)).zfill(9)
            else:
                seconds = '%02d' % seconds

            string     = ['P%04d' % self._years]
            string.append('-%02d' % self._months)
            string.append('-%02d' % self._days)

            string.append('T%02d' % self._hours)
            string.append(':%02d' % self._minutes)
            string.append(':%s'   % seconds)

            return ''.join(string)
        else:
            return str(self.array)
    #--- End: def 

    def __repr__(self):
        '''
x.__repr__() <==> repr(x)
'''
        return '<%s: %d intervals>' % (self.__class__.__name__, self.size)
    #--- End: def 

    def __add__(self, other):
        '''
x.__add__(y) <==> x+y

Add a TimeInterval or datetime.timedelta object, returning a new
TimeInterval object. Numpy-style broadcasting is used to match objects
of different shapes.

If other is a Time object, then a Time object is returned

**Examples**

>>> i = TimeInterval(days=[1,1])
>>> j = TimeInterval(days=2, seconds=[10000, 20000])
>>> print i+j
[P0000-00-03T02:46:40 P0000-00-03T05:33:20]

>>> i = TimeInterval(days=1)
>>> t = Time(60000, units='days since 1066-1-1')
>>> print i+t
[P0000-00-03T02:46:40 P0000-00-03T05:33:20]

'''
        try:
            # other is TimeInterval object
            years        = self._years         + other._years       
            months       = self._months        + other._months      
            seconds      = self._total_seconds + other._total_seconds     
            return type(self)(years=years, months=months,
                              seconds=seconds)
        except AttributeError:
            try:
                # other is datetime.timedelta object
                years        = self._years
                months       = self._months
                days         = self._days         + other.days         
                hours        = self._hours
                minutes      = self._minutes
                seconds      = self._seconds      + other.seconds     
                microseconds = self._microseconds + other.microseconds
            except AttributeError:
                try:
                    # Try for other being a Time object
                    return other + self
                except TypeError:
                    raise TypeError("Unsupported operand type(s): '"+
                                    self.__class__.__name__+"' and '"+
                                    other.__class__.__name__+"'")
            return type(self)(years, months, days, hours, 
                              minutes, seconds, microseconds)
            #--- End: try
        #--- End: try
    #--- End: def

    def __sub__(self, other):
        '''

x.__sub__(y) <==> x-y

Subtract a TimeInterval or datetime.timedelta object, returning a new
TimeInterval object. Numpy-style broadcasting is used to match objects
of different shapes.

**Examples**

>>> i = TimeInterval(months=1, days=[10, 11])
>>> j = TimeInterval(hours=1)
>>> print i-j
[P0000-01-09T23:00:00 P0000-01-10T23:00:00]

'''
        return self + (-other)

    def __mul__(self, other):
        '''

x.__mul__(y) <==> x*y

Multiply a TimeInterval by a number or sequence of numbers, returning
a new TimeInterval object. Numpy-style broadcasting is used to match
objects of different shapes.

**Examples**

>>> i = TimeInterval(months=1, days=[10, 11])
>>> print i*2
[P0000-02-20T00:00:00 P0000-02-22T00:00:00]
>>> print i*[2,4]
[P0000-02-20T00:00:00 P0000-04-44T00:00:00]
'''

        years   = self._years         * other
        months  = self._months        * other
        seconds = self._total_seconds * other
        
        if (years % 1).any():           
            raise ValueError, 'Non-integer number of years'

        if (months % 1).any():            
            raise ValueError, 'Non-integer number of months'

        return type(self)(years   = numpy.int64(numpy.around(years)), 
                          months  = numpy.int64(numpy.around(months)),
                          seconds = seconds)
    #--- End: def

    def __div__(self, other):
        '''
x.__div__(y) <==> x/y

Divide a TimeInterval by a number, returning a new TimeInterval
object. Numpy-style broadcasting is used to match objects of different
shapes.

TODO: allow other to be sequence of numbers, cf. __mul__

**Examples**

>>> i = TimeInterval(months=4, days=[10, 11])
>>> print i/2
[P0000-02-05T00:00:00 P0000-02-05T12:00:00]
'''
        return self * (1.0/other)
    #--- End: def

    def __iadd__(self, other):
        '''
x.__iadd__(y) <==> x+=y
'''
        return self + other
    #--- End: def

    def __isub__(self, other):
        '''
x.__isub__(y) <==> x-=y
'''
        return self - other
    #--- End: def

    def __imul__(self, other):
        '''
x.__imul__(y) <==> x*=y
'''
        return self * other
    #--- End: def

    def __idiv__(self, other):
        '''
x.__div__(y) <==> x/=y
'''
        return self * (1.0/other)
    #--- End: def

    def __pos__(self):
        '''
x.__pos__() <==> +x
    '''
        return self * 1
    #--- End: def

    def __neg__(self):
        '''
x.__neg__() <==> -x
    '''
        return self * -1
    #--- End: def

    def __eq__(self, other):
        '''
x.__eq__(y) <==> x==y

Return a numpy boolean array showing where two TimeInterval object are
the same.
'''
        if not isinstance(other, TimeInterval):
            raise TypeError, "Bad operand type(s): '%s' and '%s'" % (self.__class__.__name__,
                                                                     other.__class__.__name__)
        
        smonths = 12*self._years  + self._months
        omonths = 12*other._years + other._months
        if (smonths != omonths).any():
            return False
        
        return numpy.absolute(self._total_seconds-other._total_seconds) <= 1e-9
    #--- End: def

    def __ne__(self, other):
        '''
x.__ne__(y) <==> x!=y
'''
        return ~(self == other)
    #--- End: def
    
    def __lt__(self, other):
        '''
x.__lt__(y) <==> x<y

Return a numpy boolean array showing where one TimeInterval object is
strictly less than another.

NOTE: I seem to be a bit confused about the seperation of years and
      months with the other periods, here.
'''
        if not isinstance(other, TimeInterval):
            raise TypeError, "Bad operand type(s): '%s' and '%s'" % \
                (self.__class__.__name__, other.__class__.__name__)
        try:
            smonths = self._years  * 12 + self._months
            omonths = other._years * 12 + other._months
            if smonths.any() and omonths.any():
                return smonths < omonths
            
            if self.size == other.size == 1:
                self._total_seconds < other._total_seconds
            else:
                return self.array < other.array
        except AttributeError:
            raise TypeError, 'bad < things dch'
        return self._total_seconds < other._total_seconds
    #--- End: def         

    def _interval_getattr(self, attr):
        value = getattr(self, attr)
        if value.size < self.size:
            new = numpy.empty(self.shape, dtype='int64')
            new.fill(value)
            setattr(self, attr, new)
            return new
        else:
            return getattr(self, attr)
    #--- End: def 

    @property
    def years(self):
        '''

Return the years of each interval in the TimeInterval as a numpy array
with the same shape as the TimeInterval.

**Examples**

>>> TimeInterval(years=[1,2,3]).years
[1, 2, 3]
'''
        return self._interval_getattr('_years')
    #--- End: def 

    @property
    def months(self):
        '''

Return the months of each interval in the TimeInterval as a numpy
array with the same shape as the TimeInterval.

**Examples**

>>> TimeInterval(months=[1,2,3]).months
[1, 2, 3]
'''
        return self._interval_getattr('_months')
    #--- End: def 

    @property
    def days(self):
        '''

Return the days of each interval in the TimeInterval as a numpy array
with the same shape as the TimeInterval.

**Examples**

>>> TimeInterval(years=[1,2,3]).years
[1, 2, 3]
'''
        return self._interval_getattr('_days')
    #--- End: def 

    @property
    def hours(self):
        '''

Return the hours of each interval in the TimeInterval as a numpy array
with the same shape as the TimeInterval.

**Examples**

>>> TimeInterval(hours=[1,2,3]).hours
[1, 2, 3]
'''
        return self._interval_getattr('_hours')
    #--- End: def 

    @property
    def minutes(self):
        '''

Return the minutes of each interval in the TimeInterval as a numpy
array with the same shape as the TimeInterval.

**Examples**

>>> TimeInterval(minutes=[1,2,3]).minutes
[1, 2, 3]
'''
        return self._interval_getattr('_minutes')

    @property
    def seconds(self):
        '''

Return the seconds of each interval in the TimeInterval as a numpy
array with the same shape as the TimeInterval.

**Examples**

>>> TimeInterval(seconds=[1,2,3]).seconds
[1, 2, 3]
'''
        return self._interval_getattr('_seconds')
    #--- End: def 

    @property
    def microseconds(self):
        '''

Return the microseconds of each interval in the TimeInterval as a
numpy array with the same shape as the TimeInterval.

**Examples**

>>> TimeInterval(microseconds=[1,2,3]).microseconds
[1, 2, 3]
'''
        return self._interval_getattr('_microseconds')
    #--- End: def 

    def replace(self, years=None, months=None,
                 days=None, hours=None, minutes=None, 
                 seconds=None, microseconds=None):
        '''

cf. datetime.datetime.replace. NOTE: should this method be binned in
favour of __setitem__?
        '''
        new = copy.deepcopy(self)

        [setattr(new, '_%s' % attr, value) \
             for attr, value in zip(('years', 'months', 'days', 'hours', 
                                     'minutes', 'seconds', 'microseconds'),
                                    ( years ,  months ,  days ,  hours , 
                                      minutes ,  seconds, microseconds)) \
             if value is not None]


        # Reset attributes
        new._adjust_attributes()

        return new
    #--- End: def

    def resolution(self):
        '''
cf. datetime.timedelta.resolution

Currently returns 1 nanosecond
'''
        return type(self)(microseconds=0.001)
    #--- End: def 

#--- End: class
